package br.edu.ufcg.greengrid.model.machine;

import java.util.HashMap;
import java.util.Map;

import static br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;

import br.edu.ufcg.greengrid.model.Grid;
import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.machine.state.MachineState;
import br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;
import br.edu.ufcg.greengrid.model.machine.state.factory.MachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.NormalMachineStateFactory;

import com.mallardsoft.tuple.Pair;
import com.mallardsoft.tuple.Tuple;


public class Machine implements Comparable<Machine> {

	public static MachineStateFactory DEFAULT_MACHINE_STATE_FACTORY = new NormalMachineStateFactory();
	
	public static long DEFAULT_OPERATIONS_PER_TIME = 1L;
	
	private String id;

	private Job job;

	private MachineState state;
	
	private Map<Pair<MSEnum, MSEnum>, Integer> stateTransitionCounter;
	
	private Map<MSEnum, MachineState> states;
	
	private Grid grid;
	
	private long operationsPerSecond;
	
	private static Map<MSEnum, MachineState> getStates() {
		return DEFAULT_MACHINE_STATE_FACTORY.getStates();
	}
	
	public Machine(String id) {

		this(null, id);
	}

	public Machine(Grid grid, String id) {
		
		this(grid, id, getStates(), DEFAULT_OPERATIONS_PER_TIME);
	}
	
	public Machine(Grid grid, String id, Map<MSEnum, MachineState> states, long operationsPerSecond) {
		this.grid = grid;
		this.id = id;
		this.states = states;
		this.state = this.states.get(MSEnum.USER);
		this.stateTransitionCounter = new HashMap<Pair<MSEnum, MSEnum>, Integer>();
		this.operationsPerSecond = operationsPerSecond;
	}

	public boolean done() {

		return !this.state.getType().equals(MSEnum.GRID);
	}

	public Grid getGrid() {
		return grid;
	}

	public String getId() {

		return id;
	}
	
	public long getOperationsPerSecond() {
	
		return operationsPerSecond;
	}
	
	public void setOperationsPerSecond(long operationsPerSecond) {
	
		this.operationsPerSecond = operationsPerSecond;
	}

	public void setIdle(long idletime) {

		this.state.setIdle(this, idletime);
	}

	public void update() {

		//TODO: The method "changeStateIfNeeded" end up being called twice by Grid, because it calls changeStateIfNeeded() and then update().
		//The grid calls changeStateIfNeeded() in order to make machines not running any jobs available before scheduling
		//new jobs. Here I'm calling changeStateIfNeeded() again because the Unit tests aren't calling it before call update()
		//We need to refactor the unit tests and remove the call here or think another way.
		this.state.changeStateIfNeeded(this);
		this.state.update(this);
		this.state.changeStateIfNeeded(this);
	}
	
	public Job getJob() {

		return job;
	}
	
	
	public void setJob(Job job) {
		if(job != null){
			job.normalizeRuntime(this.operationsPerSecond);
		}
		this.job = job;
	}

	public void run(Job job) {

		this.state.run(this, job);
	}

	public boolean isIdle() {

		return isAtState(MSEnum.IDLE);
	}

	public boolean isBeingUsedByUser() {

		return isAtState(MSEnum.USER);
	}

	public boolean hasJobAllocated() {

		return this.job != null;
	}

	public void changeState(MSEnum newStateType, long countdown){
		//Accounting the change of the state
		MachineState oldState = this.state;
		MSEnum oldStateType = oldState.getType();
		assert !oldStateType.equals(newStateType) : String.format("Machine: %s was trying to change to the same state it was already: %s", this.id, oldStateType);
		Pair<MSEnum, MSEnum> key = Tuple.from(oldStateType, newStateType);
		Integer counter = this.stateTransitionCounter.get(key);
		if(counter == null){
			counter = 0;
		}
		counter++;
		this.stateTransitionCounter.put(key, counter);
		
		//Updating the current state to new state
		MachineState newState = this.states.get(newStateType);
		newState.setCountdown(countdown);
		this.state = newState;
		
		this.grid.machineChangedState(this, oldStateType, newStateType);
		
	}
	
	public void canceledJob(Job job){
		if(this.grid != null){
			this.grid.canceledJob(job);
		}
	}
	
	public int getNumTransitions(MSEnum from, MSEnum to){
		Integer counter = this.stateTransitionCounter.get(Tuple.from(from, to));
		if(counter == null){
			counter = 0;
		}
		
		return counter;
	}
	
	public long getTimeSpentAtCurrentState() {
		
		return this.state.getTimeSpent();
	}
	
	public long getTotalTimeSpentAtState(MSEnum stateType){
		
		MachineState state = this.states.get(stateType);
		return state != null ? state.getTotalTimeSpent() : 0L;
	}

	public boolean isAtState(MSEnum stateType) {

		return getCurrentStateType().equals(stateType);
	}
	
	public MSEnum getCurrentStateType() {
		
		return this.state.getType();
	}

	public void wakeUp() {

		this.state.wakeUp(this);
	}

	@Override
	public int compareTo(Machine o) {
		
		return o.id.compareTo(o.id);
	}

	public long getTotalTimeSpentAtAllStates() {

		long timeSpent = 0L;
		
		for (MSEnum state : MSEnum.values()) {
			timeSpent += getTotalTimeSpentAtState(state);
		}
		
		return timeSpent;
	}

}
